package org.dftproject.lineagelinkage.core;

import java.io.File;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dftproject.genesis.core.persistence.IPersistable;
import org.dftproject.genesis.core.persistence.bdb.PersistenceBinding;
import org.dftproject.genesis.data.genealogy.IPerson;
import org.eclipse.core.runtime.Platform;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.SynchronousBundleListener;

import com.sleepycat.je.Cursor;
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;

public class LocalDuplicateRepository implements IDuplicateRepository {

	private static final Log log = LogFactory.getLog(LocalDuplicateRepository.class);
	
	private Environment environment;
	
	public LocalDuplicateRepository() {
		File environmentFile = new File(getPath());
		environmentFile.mkdirs();
	
		try {
			EnvironmentConfig environmentConfig = new EnvironmentConfig();
			environmentConfig.setAllowCreate(true);
			environment = new Environment(environmentFile, environmentConfig);
			
			Activator.getDefault().getBundle().getBundleContext().addBundleListener(new SynchronousBundleListener() {
	
				public void bundleChanged(BundleEvent event) {
					if (event.getType() == BundleEvent.STOPPING)
						close();
				}
	
			});
		} catch (DatabaseException e) {
			log.error("Failed to open local duplicate repository environment", e);
		}
	}
	
	public String getPath() {
		return Platform.getInstanceLocation().getURL().getPath().substring(1) + "duplicates";
	}
	
	public void close() {
		if (environment != null) {	
			try {
				environment.close();
			} catch (DatabaseException e) {
				log.error("Failed to close local duplicate repository environment", e);
			}
			
			environment = null;
		}
	}
	
	protected Database getDatabase() {
		try {
			DatabaseConfig repositoryConfig = new DatabaseConfig();
			repositoryConfig.setAllowCreate(true);
			repositoryConfig.setSortedDuplicates(true);
			return environment.openDatabase(null, "duplicates", repositoryConfig);
		} catch (DatabaseException e) {
			log.error("Failed to open local duplicate repository database", e);
		}
	
		return null;
	}
	
	public Set<IPerson> getPrimaryPersons() {
		Set<IPerson> persons = new HashSet<IPerson>();
		
		Database database = null;
		Cursor cursor = null;

		try {
			database = getDatabase();
			if (database != null) {
				cursor = database.openCursor(null, CursorConfig.DEFAULT);
				if (cursor != null) {
					DatabaseEntry key = new DatabaseEntry();
					DatabaseEntry data = new DatabaseEntry();

					while (cursor.getNextNoDup(key, data, LockMode.DEFAULT) == OperationStatus.SUCCESS) {
						IPerson person = (IPerson) new PersistenceBinding().entryToObject(key);
						if (person != null)
							persons.add(person);
					}
				}
			}
		} catch (DatabaseException e) {
			log.error("Failed to get primary persons", e);
		} finally {
			try {
				if (cursor != null)
					cursor.close();
				if (database != null)
					database.close();
			} catch (DatabaseException e) {
			}
		}
		
		return persons;
	}
	
	public Set<IPerson> getPotentialDuplicates(IPerson person) {
		Set<IPerson> duplicates = new HashSet<IPerson>();
		if (!(person instanceof IPersistable))
			return duplicates;
		
		Database database = null;
		Cursor cursor = null;

		try {
			database = getDatabase();
			if (database != null) {
				cursor = database.openCursor(null, CursorConfig.DEFAULT);
				if (cursor != null) {
					DatabaseEntry key = new DatabaseEntry();
					new PersistenceBinding().objectToEntry(person, key);
					
					DatabaseEntry data = new DatabaseEntry();

					OperationStatus status = cursor.getSearchKey(key, data, LockMode.DEFAULT);
					while (status == OperationStatus.SUCCESS) {
						IPerson duplicate = (IPerson) new PersistenceBinding().entryToObject(data);
						if (duplicate != null)
							duplicates.add(duplicate);
						
						status = cursor.getNextDup(key, data, LockMode.DEFAULT);
					}
				}
			}
		} catch (DatabaseException e) {
			log.error("Failed to get duplicates of " + person, e);
		} finally {
			try {
				if (cursor != null)
					cursor.close();
				if (database != null)
					database.close();
			} catch (DatabaseException e) {
			}
		}
		
		return duplicates;
	}
	
	public void addPotentialDuplicate(IPerson person, IPerson duplicate) {
		if (!(person instanceof IPersistable) || !(duplicate instanceof IPersistable)) {
			log.error("Failed to add duplicate because either the primary or duplicate person was not persistable");
			return;
		}
		
		Database database = null;
		
		try {
			database = getDatabase();
			if (database != null) {
				DatabaseEntry key = new DatabaseEntry();
				new PersistenceBinding().objectToEntry(person, key);
				
				DatabaseEntry data = new DatabaseEntry();
				new PersistenceBinding().objectToEntry(duplicate, data);
				
				if (database.getSearchBoth(null, key, data, LockMode.DEFAULT) == OperationStatus.NOTFOUND)
					database.put(null, key, data);
			}
		} catch (Exception e) {
			log.error("Failed to store duplicate of " + person, e);
		} finally {
			try {
				if (database != null)
					database.close();
			} catch (DatabaseException e) {
			}
		}
	}

	public void removePotentialDuplicate(IPerson person, IPerson duplicate) {
		Database database = null;
		Cursor cursor = null;
		
		try {
			database = getDatabase();
			if (database != null) {
				cursor = database.openCursor(null, CursorConfig.DEFAULT);
				if (cursor != null) {
					DatabaseEntry key = new DatabaseEntry();
					new PersistenceBinding().objectToEntry(person, key);
					
					DatabaseEntry data = new DatabaseEntry();
					new PersistenceBinding().objectToEntry(duplicate, data);
					
					if (cursor.getSearchBoth(key, data, LockMode.DEFAULT) == OperationStatus.SUCCESS)
						cursor.delete();
				}
			}
		} catch (Exception e) {
			log.error("Failed to remove duplicate of " + person, e);
		} finally {
			try {
				if (cursor != null)
					cursor.close();
				if (database != null)
					database.close();
			} catch (DatabaseException e) {
			}
		}
	}

	public long getDuplicateCount() {
		Database database = null;

		try {
			database = getDatabase();
			if (database != null)			
				return database.count();
		} catch (Exception e) {
			log.error("Failed to get duplicate count", e);
		} finally {
			try {
				if (database != null)
					database.close();
			} catch (DatabaseException e) {
			}
		}
		
		return 0;
	}

}
